#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os
import stat
import sys

from common.cleaner import clear
from common.common import execute_cmd, check_command_injection, execute_cmd_list
from common.const import AuthType, ExecuteResultEnum, SysData
from sqlserver import log
from sqlserver.commons.common import get_key_value, check_del_dir_or_file, exec_pw_shell_filename, output_tmp_info
from sqlserver.commons.const import ParamConstant, JobData, SQLServerStrConstant
from sqlserver.commons.const import SqlCmd, CodeIdConst
from sqlserver.commons.const import TmpFile, SQLServerCode, BodyErr
from sqlserver.commons.sqlserver_parse_param import VDIAPINAME
from sqlserver.commons.sqlserver_utils import SqlServerUtils
from sqlserver.sqlserver_base import SQLServerBase
from sqlserver.sqlserver_resource import SqlServerResource


class SqlServerClusterInstanceResource:
    def __init__(self, p_id, job_id, json_param):
        self.m_resoure_base = SqlServerResource()
        self._p_id = p_id
        self._job_id = job_id
        self._json_param = json_param
        self.m_instance_name = ""
        self.m_network_name = ""
        self.m_authentication_type = 0
        self.m_check_output = {'code': SQLServerCode.FAILED.value, 'bodyErr': 0, 'message': ''}
        self.m_resource_output = {'resourceList': []}
        self.m_username = ""
        self.pass_prefix = ""
        self.user_info = ""
        self.m_cluster_name = "Cluster"
        self.m_func_type = ""
        self.m_default = "MSSQLSERVER"
        self.sql_utils = None

    @staticmethod
    def write_to_ps_script(cmd):
        tmp_script = os.path.join(ParamConstant.PARAM_FILE_PATH, "temp.ps1")
        flags = os.O_WRONLY | os.O_CREAT
        modes = stat.S_IWUSR | stat.S_IRUSR
        with os.fdopen(os.open(tmp_script, flags, modes), 'w+') as out_file:
            out_file.write(cmd)

    @staticmethod
    def rewrite_resource_list():
        file_name = "{}{}".format("result", JobData.PID)
        file_path = os.path.join(ParamConstant.RESULT_PATH, file_name)
        if not os.path.exists(file_path):
            return {}
        with open(file_path, "r", encoding='UTF-8') as f:
            ret = f.read()
        check_del_dir_or_file(file_path)
        return json.loads(ret)

    def get_param_from_input(self):
        flags, self.m_authentication_type = get_key_value(f'application_auth_authType_{JobData.PID}')
        self.m_instance_name = self._json_param.get('application', {}).get('name', "Not exist")
        if check_command_injection(self.m_instance_name):
            log.error(f"The param: %s has special characters.", self.m_instance_name)
            raise Exception("The param has special characters.")
        self.m_network_name = self._json_param.get('application', {})\
            .get('extendInfo', {}).get('networkName', "")
        if check_command_injection(self.m_network_name):
            log.error(f"The param: %s has special characters.", self.m_network_name)
            raise Exception("The param has special characters.")
        if self.m_network_name == "":
            self.m_network_name = SqlServerUtils.get_hostname()
        if self.m_instance_name and \
                self.m_instance_name.upper() != SQLServerStrConstant.SQLSERVER_DEFAULT_SERVICES.upper():
            instance = f'\"{self.m_network_name}\\\\{self.m_instance_name}\"'
        else:
            instance = self.m_network_name
        if int(self.m_authentication_type) == AuthType.APP_PASSWORD.value:
            if not self.get_userinfo_from_env_var():
                raise Exception("Get user info Failed")
            self.user_info = f"-S {instance} -U {self.m_username}"
        elif int(self.m_authentication_type) == AuthType.OS_PASSWORD.value:
            self.user_info = f"-S {instance}"
        else:
            log.error(f"Auth mode: {self.m_authentication_type} is not support")
            raise Exception("Auth mode not support")
        self.sql_utils = SqlServerUtils(self.m_authentication_type, self.user_info, self.pass_prefix, self._job_id)

    def get_userinfo_from_env_var(self):
        flags, self.m_username = get_key_value(f'application_auth_authKey_{JobData.PID}')
        if not flags:
            return False
        flags, m_password = get_key_value(f"application_auth_authPwd_{JobData.PID}")
        if not flags:
            return False
        if not self.m_username or not m_password:
            log.error("Get user info failed")
            return False
        clear(m_password)
        if check_command_injection(self.m_username):
            log.error(f"The param: %s has special characters.", self.m_username)
            raise Exception("The param has special characters.")
        self.pass_prefix = f"application_auth_authPwd_{JobData.PID}"
        return True

    def judgments_cluster_name(self):
        param_name = self._json_param.get('appEnv', {}).get('name', "Not exist")
        if param_name == "Not exist":
            return True
        if param_name == self.m_cluster_name:
            return True
        else:
            return True

    def get_param_nodes_name(self):
        nodes_list = self._json_param.get('appEnv', {}).get('extendInfo', {})
        name_list = nodes_list.get('allNodes', [])
        if not name_list:
            return False, []
        all_name_list = name_list.split(',')
        result_list = []
        for member in all_name_list:
            temp_name = member
            if temp_name == "Not exist":
                log.error('Key name not exist')
                return False, []
            result_list.append(temp_name)
        return True, result_list

    def judgments_cluster_node(self, std_out):
        indexs = 0
        flags, name_list = self.get_param_nodes_name()
        if not flags:
            return False
        member_list = []
        std_out_lens = len(std_out) - 3
        for mem_info in std_out:
            if indexs < 3:
                indexs += 1
                continue
            if indexs == std_out_lens:
                break
            indexs += 1
            ret_str = mem_info.split()
            if len(ret_str) <= 0:
                log.error("Get cluster node failed")
                return False
            member_list.append(ret_str[0])
        if len(name_list) != len(member_list):
            log.error("The number of cluster nodes is incorrect")
            return False
        name_list_odr = sorted(name_list)
        member_list_odr = sorted(member_list)
        indexs = 0
        for mems in name_list_odr:
            if mems != member_list_odr[indexs]:
                log.error(f'Cluster: {self.m_cluster_name} not include nodes : {mems}')
                return False
            indexs += 1
        return True

    def exec_pw_shell(self, shell_cmd):
        try:
            self.write_to_ps_script(shell_cmd)
        except Exception as e:
            self.m_resoure_base.set_exception_info(SQLServerCode.FAILED.value, str(e))
            return False, []
        tmp_script = os.path.join(ParamConstant.PARAM_FILE_PATH, "temp.ps1")
        cmds = f'{VDIAPINAME.ps_loc} {tmp_script}'
        ret, std_out, std_err = execute_cmd(cmds)
        try:
            os.remove(tmp_script)
        except Exception as err:
            log.error(f"Delete file temp.ps1 failed: {err}!")
        if int(ret) != ExecuteResultEnum.SUCCESS:
            return False, []
        return True, std_out.split('\n')

    def check_pw_shell_exec_policy(self):
        log.info(f"Start to check powershell execution policy{self._job_id}.")
        flags, std_out, std_err = exec_pw_shell_filename(SqlCmd.EXECUTION_POLICY)
        if not flags:
            return True
        return False

    def write_result_to_file(self, in_str):
        log.info(f"Enter write_result_to_file {self._job_id}. ")
        file_name = "{}{}".format("result", JobData.PID)
        file_path = os.path.join(ParamConstant.RESULT_PATH, file_name)
        log.info(f'{file_path}')
        output_tmp_info(file_path, in_str)
        log.info("Leave write_result_to_file")

    def authentication(self):
        log.debug("Enter authentication....")
        if self.check_pw_shell_exec_policy():
            log.error("Powershell execution permission denied.")
            self.m_check_output['code'] = SQLServerCode.FAILED.value
            self.m_check_output['bodyErr'] = BodyErr.NO_WINDOWS_POWER_SHELL_PERMISSION.value
            self.m_check_output['message'] = "Powershell execution permission denied."
            self.write_result_to_file(json.dumps(self.m_check_output))
        flags, std_out = self.exec_pw_shell(SqlCmd.CLUSTER_INFO)

        if not flags:
            log.error(std_out)
            return False
        self.m_cluster_name = std_out[3]
        if not self.judgments_cluster_name():
            self.m_resoure_base.set_exception_info(SQLServerCode.FAILED.value,
                                                   f'The clsuter name {self.m_cluster_name} is not: '
                                                   f'{self.m_cluster_name}')
            return False
        flags, std_out = self.exec_pw_shell(SqlCmd.CLUSTER_NODE)
        if not flags:
            return False
        if not self.judgments_cluster_node(std_out):
            self.m_resoure_base.set_exception_info(SQLServerCode.FAILED.value,
                                                   "The nodes do not belong o the same cluster")
            return False
        self.m_resoure_base.set_exception_info(0, "Is a cluster")
        log.debug("Leave authentication....")
        return True

    def rewrite_result(self, extend_info):
        file_name = "{}{}".format("result", JobData.PID)
        file_path = os.path.join(ParamConstant.RESULT_PATH, file_name)
        if not os.path.exists(file_path):
            return
        with open(file_path, "r", encoding='UTF-8') as f:
            ret = f.read()
        ret_json = json.loads(ret)
        check_del_dir_or_file(file_path)
        self.m_check_output['code'] = ret_json.get('code')
        self.m_check_output['bodyErr'] = ret_json.get('bodyErr')
        self.m_check_output['message'] = json.dumps(extend_info)
        self.m_resoure_base.write_result_to_file(json.dumps(self.m_check_output))

    def auth_cluster_instance(self):
        self.get_param_from_input()
        if self.m_instance_name and \
                self.m_instance_name.upper() != SQLServerStrConstant.SQLSERVER_DEFAULT_SERVICES.upper():
            sc_query = 'sc query MSSQL$%s ' % self.m_instance_name
        else:
            sc_query = 'sc query %s ' % self.m_instance_name
        cmd_list = [sc_query, " findstr STATE"]
        ret, std_out, std_err = execute_cmd_list(cmd_list)
        if int(ret) != 0:
            log.error("Failed to check instance status")
            raise Exception("Auth failed")
        if "RUNNING" not in std_out:
            log.warn("SQL Server services do not running")
            return True, False
        cmd = f'sqlcmd {self.user_info} -Q "exit;"'
        ret, std_out, std_err = self.sql_utils.get_command_result(cmd)
        if ret != "0":
            log.error(f'Auth failed, Err code: {ret}')
            raise Exception("Auth failed")
        return True, True

    def authentication_v2(self):
        log.debug("Enter authentication v2....")
        extend_info = {
            "role": 0,
            "state": False,
            "name": ""
        }
        m_check_output = {'code': 0, 'bodyErr': 0, 'message': ''}
        try:
            flags, state = self.auth_cluster_instance()
            extend_info['name'] = self.m_network_name
            extend_info['state'] = state
        except Exception as e_info:
            if "Auth failed" in str(e_info):
                m_check_output['code'] = BodyErr.SQLSERVER_AUTH_INFO_ERROR.value
                m_check_output['bodyErr'] = BodyErr.SQLSERVER_AUTH_INFO_ERROR.value
            else:
                m_check_output['code'] = SQLServerCode.FAILED.value
                m_check_output['bodyErr'] = SQLServerCode.FAILED.value
            flags = False
        if flags:
            extend_info['role'] = 1
        version = self.sql_utils.get_version()
        extend_info['version'] = version
        m_check_output['message'] = json.dumps(extend_info)
        self.m_resoure_base.write_result_to_file(json.dumps(m_check_output))
        return True

    def get_ag_infos(self):
        sql_cmd = "select T1.ag_name, T1.ag_id, T.ag_resource_id from sys.dm_hadr_instance_node_map T " \
                  "left join sys.dm_hadr_name_id_map T1 on T1.ag_resource_id = T.ag_resource_id inner join " \
                  "sys.dm_hadr_availability_group_states T3 on T1.ag_id = T3.group_id"
        cmds = f'sqlcmd {self.user_info} -Q "{sql_cmd}" -W -f {CodeIdConst.WINDOWS_CODE} -o "{TmpFile.OUT_FILE}"'
        try:
            ret, std_out, std_err = self.sql_utils.get_command_result(cmds)
        except Exception as e_info:
            log.error('Exec failed')
            self.m_resoure_base.set_exception_info(SQLServerCode.FAILED.value, str(e_info))
            return False, ""
        if int(ret) != 0:
            log.error(f'Exec failed, errcode={ret}')
            self.m_resoure_base.set_exception_info(SQLServerCode.FAILED.value, std_err)
            return False, ""
        result = self.m_resoure_base.read_tmp_result_file()
        check_del_dir_or_file(TmpFile.TMPSQLV1_FILE)
        return True, result.split("\n")[2:-2]

    def add_ag_list_to_result(self):
        self.get_param_from_input()
        flags, ans_json = self.get_ag_infos()
        if not flags:
            log.error('Get ag info failed')
            return False, []
        resource = self.rewrite_resource_list()
        resource_list = resource.get('resourceList', [])
        for ans_member in ans_json:
            ag_name = ans_member.split(" ")[0]
            ag_id = ans_member.split(" ")[1]
            ag_resource_id = ans_member.split(" ")[2]
            t_list = self.sql_utils.get_ag_node_info(ag_resource_id)
            host_name = self.sql_utils.list_to_str(t_list)
            node_exend = {
                'status': 1,
                'instanceName': self.m_instance_name,
                'hostname': self.m_network_name,
                'endpoint': host_name
            }
            list_mbr = {
                'extendInfo': node_exend,
                'name': ag_name,
                'role': '',
                'subType': 'SQLServer-alwaysOn',
                'type': 'Database',
                'id': ag_id
            }
            resource_list.append(list_mbr)
        return True, resource

    def list_application_v2(self):
        log.info('Enter list_application_v2')
        self.m_resoure_base.list_appliction_resource()
        flags, result = self.add_ag_list_to_result()
        if not flags:
            log.error("Add ag info to resource list failed...")
            self.m_resoure_base.write_result_to_file(json.dumps(result))
            return False
        self.m_resoure_base.write_result_to_file(json.dumps(result))
        log.info('Leave list_application_v2')
        return True


if __name__ == '__main__':
    log.info("Running main....")
    args = sys.argv[1:]
    if len(args) < 2:
        log.error("Parameters not enough")
        sys.exit(1)
    for line in sys.stdin:
        SysData.SYS_STDIN = line
        break
    JobData.PID = args[1]
    FILE_NAME = "{}{}".format("param", JobData.PID)
    FILE_PATH = os.path.join(ParamConstant.PARAM_FILE_PATH, FILE_NAME)
    if not os.path.exists(FILE_PATH):
        log.info(f'Path: {FILE_PATH} not exist')
        sys.exit(1)
    JSON_TMP = SQLServerBase.read_temp_file(FILE_PATH)
    newInstance = SqlServerClusterInstanceResource(JobData.PID, '', JSON_TMP)
    if not newInstance.m_resoure_base.check_result_path():
        sys.exit(1)
    newInstance.m_resoure_base.get_winows_input(args[0])
    if newInstance.m_resoure_base.m_func_type == "CheckApplication":
        newInstance.authentication_v2()
        check_del_dir_or_file(FILE_PATH)
        clear(SysData.SYS_STDIN)
    elif newInstance.m_resoure_base.m_func_type == "ListApplicationResource":
        newInstance.list_application_v2()
        check_del_dir_or_file(FILE_PATH)
        clear(SysData.SYS_STDIN)
    else:
        log.error("Wrong input from command line!")
        sys.exit(1)
